package mini.memory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.Stack;

import mini.expr.Element;
import mini.expr.lists.ElementList;

public class MemoryManager {
    private static MemoryManager instance = new MemoryManager();
    private ConsCell[] heap = null;
    private int heapSize = 8196; //Use a default heap size of 8196.
    private Stack<Integer> availStack = new Stack<Integer>();
    
    private MemoryManager() {
        initializeHeap();
    }
    
    public static MemoryManager getInstance() {
        return instance;
    }
    
    /**
     * Stores the given element in a memory cell and returns the memory address
     * of the stored element. If the element cannot be stored, null is returned.
     * 
     * @param element the element to store
     * @param existingListAddress the memory address of the existing list. If 
     * this field is not null, element will be placed at the head of the list 
     * stored at the address.
     * 
     * @return the memory address of element or null if the element isn't stored.
     */
    public Integer store(Element element, Integer existingListAddress) {
        if (element == null) {
            throw new IllegalArgumentException("Element cannot be null");
        }
        
        if (availStack.size() == 0) {
            return null;
        }
        
        int availMemoryAddress = availStack.pop();
        
        heap[availMemoryAddress].setCar(element);
        
        if (existingListAddress == null) {
            heap[availMemoryAddress].setCdr(null);
        } else {
            heap[availMemoryAddress].setCdr(heap[existingListAddress]);
        }
        
        return availMemoryAddress;
    }
    
    /**
     * Load the list of cells located at the specified memory address.
     * 
     * @param memoryAddress
     * @return
     */
    public List<Element> loadList(Integer memoryAddress) {
        if (memoryAddress == null) {
            throw new IllegalAccessError("Memory address cannot be null");
        }
        
        List<Element> elements = new ArrayList<Element>();
        ConsCell cell = heap[memoryAddress];

        while (cell != null) {
            elements.add(cell.getCar());
            
            cell = cell.getCdr();
        }
        
        return elements;
    }
    
    private void printHeap() {
    	System.out.println( "HEAP of length " + heap.length );
    	for( int i = 0; i < heap.length; i++ ) {
    		System.out.println( "  heap[" + i + "] = " + heap[i] );
    	}
	}

	public Integer getElementAddressAtIndex(Integer memoryAddress, int index) {
        ConsCell consCell = heap[memoryAddress];
        int counter = 0;
        
        while (consCell != null) {
            if (counter == index) {
                return consCell.getAddress();
            }
            
            consCell = consCell.getCdr();
            counter++;
        }
        
        return null;
    }
    
    public void garbageCollect(HashMap<String, Element> nametable) {
        if (nametable == null) {
            throw new IllegalArgumentException("nametable cannot be null");
        }

        //printHeap();

        //Remove all available memory cells
        availStack.clear();
        
        //Mark used cells
        mark(nametable);
        
        //Collect all unused cells
        sweep();
    }
    
    private void sweep() {
        for (int i = 0; i < heapSize; i++) {
            if (heap[i].isMarked()) {
                heap[i].setMarked(false);
            } else {
                availStack.push(i);
            }
        }
    }

    private void mark(HashMap<String, Element> nametable) {
        // Iterate through name table and mark all memory cells currently being
        // used by lists.
        for(Element element : nametable.values()) {
            //If it's a list it'll contain the memory address of the first element
            //in the list
            if (element.isList()) {
                //This should not be null if the code gets here
                ElementList elementList = element.getListValue();
                
                Integer memoryAddress = elementList.getSequence().getMemoryAddress();
                
                //Empty lists don't have a memory address.
                if (memoryAddress != null) {
                	//Now recursively mark all the cells that make up the list
                	mark(heap[memoryAddress]);
                }
            }
        }
        
    }

    private void mark(ConsCell consCell) {
        if (consCell.isMarked()) {
            return;
        }
        
        consCell.setMarked(true);
        
        Element element = consCell.getCar();
        
        if (element.isList()) {
        	Integer memoryAddress = element.getListValue().getSequence().getMemoryAddress();
        	
        	if (memoryAddress != null) {
        		mark(heap[memoryAddress]);
        	}
        }
        
        ConsCell cdr = consCell.getCdr();
        
        if (cdr != null) {
            mark(cdr);
        }
        
    }

    private void initializeHeap() {
        try {
            Properties heapProps = new Properties();
            
            heapProps.load(MemoryManager.class.getResourceAsStream("/heap.properties"));
            
            String heapSizeString = heapProps.getProperty("heap.size", Integer.toString(heapSize));
            heapSize = Integer.parseInt(heapSizeString);            
        } catch (IOException e) {
            System.out.println("Error while loading heap size. Default heap size will be used.");
        }
        
        heap = new ConsCell[heapSize];
        
        for(int i = (heapSize - 1); i >= 0; i--) {
            heap[i] = new ConsCell();
            
            heap[i].setAddress(i);
            
            availStack.push(i);
        }
    }
}
